﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

namespace Notoric.Model.Elements.CWNotation
{
    /// <summary>
    /// Helper class for holding the current staff environment and transform between tones and corresponding notes.
    /// </summary>
    public class Environment
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="Environment"/> instance.
        /// </summary>
        /// <param name="clef">
        /// <para>
        /// Type: <see cref="Clef"/>
        /// </para>
        /// <para>
        /// The current clef.
        /// </para>
        /// </param>
        /// <param name="key">
        /// <para>
        /// Type: <see cref="KeySignature"/>
        /// </para>
        /// <para>
        /// The current key.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        public Environment(Clef clef, KeySignature key)
        {
            if (clef == null)
                throw new ArgumentNullException("clef");
            if (key == null)
                throw new ArgumentNullException("key");

            this.clef = clef;
            this.key = key;
            lastNotesList = new List<Note>();
            lastNotes = new ReadOnlyCollection<Note>(lastNotesList);
        }

        /// <summary>
        /// Creates a new <see cref="Environment"/> instance.
        /// </summary>
        /// <remarks>
        /// Only the <see cref="Environment.CurrentClef"/> and <see cref="Environment.CurrentKey"/>
        /// properties are copied to the created instance. The <see cref="Environment.LastNotes"/> collection
        /// is ignored.
        /// </remarks>
        /// <param name="source">
        /// <para>
        /// Type: <see cref="Environment"/>
        /// </para>
        /// <para>
        /// The environment that will be copied to the created one.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="source"/> is <see langword="null"/>.
        /// </exception>
        public Environment(Environment source)
        {
            if (source == null)
                throw new ArgumentNullException("source");

            this.CurrentClef = source.CurrentClef;
            this.CurrentKey = source.CurrentKey;
            lastNotesList = new List<Note>();
            lastNotes = new ReadOnlyCollection<Note>(lastNotesList);
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Adds a new note to the <see cref="LastNotes"/> collection.
        /// </summary>
        /// <remarks>
        /// If the <see cref="LastNotes"/> collection already contains a <see cref="Note"/> with the same pitch class and octave,
        /// it is overwritten. Otherwise, the <paramref name="note"/> is added to the end of the collection.
        /// </remarks>
        /// <param name="note">
        /// <para>
        /// Type: <see cref="Note"/>
        /// </para>
        /// <para>
        /// The added note.
        /// </para>
        /// </param>
        public void AddToLastNotes(Note note)
        {
            var ln = lastNotesList.FindIndex(n => n.PresentedTone.Note == note.PresentedTone.Note && n.PresentedTone.Octave == note.PresentedTone.Octave);
            if (ln != -1)
                lastNotesList[ln] = note;
            else
                lastNotesList.Add(note);
        }

        /// <summary>
        /// Clears the <see cref="LastNotes"/> collection.
        /// </summary>
        public void ClearLastNotes()
        {
            lastNotesList.Clear();
        }

        /// <summary>
        /// Computes the staff position and accidental of a note that correspond to a given tone.
        /// </summary>
        /// <remarks>
        /// The algorithm:
        /// <list type="number">
        /// <item>
        /// The <paramref name="staffPosition"/> is calculated by calling <see cref="Clef.GetPositionFromTone"/> of the <see cref="CurrentClef"/>.
        /// </item>
        /// <item>
        /// The <see cref="LastNotes"/> collection is searched for an occurence of the note that has the same pitch (note and octave, not accidental)
        /// as the <paramref name="tone"/>.
        /// </item>
        /// <item>
        /// If it is found and the <see cref="Note"/> has an <see cref="Accidental"/>,  the <i>actualAccidental</i> variable is set to that <see cref="Accidental"/>.
        /// Othewrise it is set to the <see cref="Accidental"/> that corresponds to the <see cref="Tone.Note"/> of the <paramref name="tone"/> in the <see cref="CurrentKey"/>.
        /// </item>
        /// <item>
        /// The <paramref name="accidental"/> is computed as <paramref name="tone"/>.<see cref="Tone.Accidental"/> - <i>actualAccidental</i>.<see cref="Accidental.Value"/>.
        /// </item>
        /// </list>
        /// </remarks>
        /// <param name="tone">
        /// <para>
        /// Type: <see cref="Tone"/>
        /// </para>
        /// <para>
        /// The tone for which the note is being computed.
        /// </para>
        /// </param>
        /// <param name="staffPosition">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// When the method returns, this parameter contains the computed staff position.
        /// </para>
        /// </param>
        /// <param name="accidental">
        /// <para>
        /// Type: <see cref="AccidentalType"/>
        /// </para>
        /// <para>
        /// When the method returns, this parameter contains the computed accidental type.
        /// </para>
        /// </param>
        public void GetNote(Tone tone, out int staffPosition, out AccidentalType accidental)
        {
            staffPosition = CurrentClef.GetPositionFromTone(tone);
            var lastNote = LastNotes.LastOrDefault(n => n.PresentedTone.Note == tone.Note && n.PresentedTone.Octave == tone.Octave);

            if (tone.Accidental != 0.0)
            {
                accidental = Accidental.AccidentalValueToType(tone.Accidental);

                if (lastNote != null)
                {
                    if (lastNote.PresentedTone.Accidental == tone.Accidental)
                        accidental = AccidentalType.None;
                }
                else if (CurrentKey[tone.Note].AccidentalType == accidental)
                    accidental = AccidentalType.None;
            }
            else
            {
                AccidentalType actualAccidental;

                if (lastNote != null && lastNote.PresentedTone.Accidental != Accidental.AccidentalTypeToValue(CurrentKey[tone.Note].AccidentalType))
                    actualAccidental = lastNote.Accidental.Type;
                else
                    actualAccidental = CurrentKey[tone.Note].AccidentalType;

                accidental = Accidental.CancelAccidental(actualAccidental);
            }
        }

        /// <summary>
        /// Computes the tone that corresponds to a given staff position.
        /// </summary>
        /// <remarks>
        /// The algorithm:
        /// <list type="number">
        /// <item>
        /// The basic tone is computed by calling the <see cref="Clef.GetToneFromPosition"/> method of the <see cref="CurrentClef"/>.
        /// </item>
        /// <item>
        /// If the <see cref="LastNotes"/> collection contains a note with the same pitch (note and octave, not accidental) that has an accidental, 
        /// the <see cref="Tone.Accidental"/> of the tone from the previous step is set to the <see cref="Accidental.Value"/> of the <see cref="Accidental"/>
        /// of that note.
        /// </item>
        /// <item>
        /// Otherwise, it is set to the <see cref="Accidental.Value"/> of the <see cref="Accidental"/> that corresponds to the <see cref="Tone.Note"/> 
        /// of the <see cref="Tone"/> from the 1st stepn in the <see cref="CurrentKey"/>.
        /// </item>
        /// </list>
        /// </remarks>
        /// <param name="staffPosition">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The staff position for which the tone is being computed.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Tone"/>
        /// </para>
        /// <para>
        /// The computed tone.
        /// </para>
        /// </returns>
        public Tone GetTone(int staffPosition)
        {
            var tone = CurrentClef.GetToneFromPosition(staffPosition);
            var lastNote = LastNotes.FirstOrDefault(n => n.PresentedTone.Note == tone.Note && n.PresentedTone.Octave == tone.Octave);

            if (lastNote != null && lastNote.Accidental.Type != AccidentalType.None)
                tone.Accidental = lastNote.Accidental.Value;
            else
                tone.Accidental = Accidental.AccidentalTypeToValue(CurrentKey[tone.Note].AccidentalType);

            return tone;
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the current clef.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Clef"/>
        /// </para>
        /// <para>
        /// The current clef.
        /// </para>
        /// </value>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="value"/> is <see langword="null"/>.
        /// </exception>
        public Clef CurrentClef
        {
            get { return clef; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                clef = value;
            }
        }

        /// <summary>
        /// Gets or sets the current key.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="KeySignature"/>
        /// </para>
        /// <para>
        /// The current key.
        /// </para>
        /// </value>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="value"/> is <see langword="null"/>.
        /// </exception>
        public KeySignature CurrentKey
        {
            get { return key; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                key = value;
            }
        }

        /// <summary>
        /// The collection of last inserterd notes.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ReadOnlyCollection{T}"/> of <see cref="Note"/>
        /// </para>
        /// <para>
        /// This collection holds the last notes that were inserted to their staff positions
        /// in this measure (or generally the scope in which they can modify the staff environment).
        /// </para>
        /// <para>
        /// In Common Western Music Notation if a <see cref="Note"/> has an <see cref="Accidental"/>,
        /// this accidental remains valid to the end of the note's measure.
        /// Consequently, all the notes on the same staff position that follows this note
        /// inherit the same accidental (and thus do not have their own) until the end of the measure
        /// is reached or a note with a different accidental is encountered.<br/>
        /// For a sake of clarity, this accidental inheritance takes place only for the notes
        /// on exactly the same staff position. Notes that have the same pitch class but another
        /// octave manage their accidentals independently.
        /// </para>
        /// </value>
        /// <seealso cref="ClearLastNotes"/>
        /// <seealso cref="AddToLastNotes"/>
        public ReadOnlyCollection<Note> LastNotes
        {
            get { return lastNotes; }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="CurrentClef"/> property.
        /// </summary>
        private Clef clef;
        /// <summary>
        /// Holds the value of the <see cref="CurrentKey"/> property.
        /// </summary>
        private KeySignature key;
        /// <summary>
        /// Holds the value of the <see cref="LastNotes"/> property.
        /// </summary>
        private ReadOnlyCollection<Note> lastNotes;
        /// <summary>
        /// Holds the collection of last notes.
        /// </summary>
        private List<Note> lastNotesList;

        #endregion
    }
}
